/*
PearReview - The peer review assistant.
Copyright (C) 2009  Dimitrij Pankratz, Anton Musichin
http://www.pearreview.com, info@pearreview.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

package com.pearreview.ui.slidebox;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.TransferHandler;
import javax.swing.event.EventListenerList;

public class SlideBoxPanel<T> extends JPanel {
	
	private static final long serialVersionUID = 775454349995446599L;
	protected FadingPanel container = new FadingPanel();
	protected JScrollPane scroll = new JScrollPane();
	protected List<SlideBox<T>> boxList = new ArrayList<SlideBox<T>>();
	protected List<T> elementList = new ArrayList<T>();
	protected DeleteAction deleteAction = new DeleteAction();
	protected JPanel pane;
	protected boolean acceptableType; // Indicates whether data is acceptable
	protected DataFlavor targetFlavor; // Flavor to use for transfer
	protected Action editAction;
	private int editIndex = -1;
	
	private EventListenerList changeListenerList = new EventListenerList();
	
	private RemoveConfirmer removeConfirmer;
	private SlideBox.RemoveConfirmer slideBoxRemoveConfirmer;
	
	/* focus logic */
	private SlideBox<?> focusSlideBox;
	private MouseListener focusMouseListener;
	
	public SlideBoxPanel(Action editAction) {
		super();
		
		this.editAction = editAction;
		
		setOpaque(false);
		//setPreferredSize(new Dimension(350,450));
		//setMaximumSize(new Dimension(350,450));
		setMinimumSize(new Dimension(300,0));
		
		
		setBorder(BorderFactory.createMatteBorder(
				1, 1, 1, 1, new Color(0x828790)));
		setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
		
		initComponents();
	}
		
	
	protected void initComponents() {
		/* focus logic */
		setFocusable(true);
		addFocusListener(new FocusListener() {
			@Override
			public void focusGained(FocusEvent e) {
				if(focusSlideBox != null) {
					focusSlideBox.setFocus(true);
				}
			}
			@Override
			public void focusLost(FocusEvent e) {
				if(focusSlideBox != null) {
					focusSlideBox.setFocus(false);
				}
			}
		});
		addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				switch(e.getKeyCode()) {
				case KeyEvent.VK_ENTER:
				case KeyEvent.VK_SPACE:
					if (focusSlideBox != null && !focusSlideBox.isOpened()) {
						focusSlideBox.slideDown(focusSlideBox.getSlideHeight());
					} else {
						focusSlideBox.slideUp(0);
					}
					break;
					
				case KeyEvent.VK_UP:
				case KeyEvent.VK_LEFT:
					int upIndex = -1;
					if(focusSlideBox != null) {
						upIndex = boxList.indexOf(focusSlideBox);
						focusSlideBox.setFocus(false);
						focusSlideBox = null;
					} else {
						upIndex = boxList.size() - 2;
					}
					
					upIndex++;
					
					if(upIndex < 0 || upIndex >= boxList.size()) {
						upIndex = boxList.size() - 1;
					}
					
					if(upIndex >= 0 && upIndex < boxList.size()) {
						focusSlideBox = boxList.get(upIndex);
						focusSlideBox.setFocus(true);
					}
					break;
					
				case KeyEvent.VK_DOWN:
				case KeyEvent.VK_RIGHT:
					int downIndex = -1;
					if(focusSlideBox != null) {
						downIndex = boxList.indexOf(focusSlideBox);
						focusSlideBox.setFocus(false);
						focusSlideBox = null;
					} else {
						downIndex = boxList.size();
					}
					
					downIndex--;
					
					if(downIndex < 0 || downIndex >= boxList.size()) {
						downIndex = 0;
					}
					
					if(downIndex >= 0 && downIndex < boxList.size()) {
						focusSlideBox = boxList.get(downIndex);
						focusSlideBox.setFocus(true);
					}
					break;
				
				case KeyEvent.VK_DELETE:
					if(focusSlideBox!=null && focusSlideBox.isDeleteEnabled()) {
						focusSlideBox.delete();
					}
					break;
					
				case KeyEvent.VK_F2:
					if(focusSlideBox!=null && focusSlideBox.isEditEnabled()) {
						focusSlideBox.edit();
					}
					break;
				}
			}
		});
		
		container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
		container.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		container.setOpaque(false);
		
		scroll.setOpaque(false);
		scroll.setHorizontalScrollBarPolicy(
				ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
		
		scroll.setViewportView(container);
		scroll.setBorder(null);
		scroll.getViewport().setOpaque(false);
		
		add(scroll);
	}
	
	public void addToList(T element) {
		addToList(element, true);
	}
	
	public MouseListener getMouseFocusListener() {
		if(focusMouseListener == null) {
			focusMouseListener = new MouseAdapter() {
				@Override
				public void mousePressed(MouseEvent e) {
					Object src = e.getSource();
					if(src instanceof SlideBox<?>) {
						requestFocusInWindow();
						if(focusSlideBox != null) {
							focusSlideBox.setFocus(false);
						}
						focusSlideBox = (SlideBox<?>) src;
						focusSlideBox.setFocus(true);
					}
				}
			};
		}
		return focusMouseListener;
	}
	
	public void setRemoveConfirmer(RemoveConfirmer newRemoveConfirmer) {
		removeConfirmer = newRemoveConfirmer;
	}
	
	public RemoveConfirmer getRemoveConfirmer(RemoveConfirmer confirmer) {
		return removeConfirmer;
	}
	
	protected SlideBox.RemoveConfirmer getSlideBoxRemoveConfirmer() {
		if(slideBoxRemoveConfirmer == null) {
			slideBoxRemoveConfirmer = new SlideBox.RemoveConfirmer () {
				@Override
				public boolean canRemove(SlideBox<?> box) {
					if(removeConfirmer != null) {
						int index = boxList.indexOf(box);
						if(index >= 0) {
							T element = elementList.get(index);
							return removeConfirmer.canRemove(SlideBoxPanel.this, 
									element, index);
						}
					}
					
					return true;
				}
			};
		}
		
		return slideBoxRemoveConfirmer;
	}
	
	public void addToList(T element, boolean update) {
		
		elementList.add(element);
		
		SlideBox<T> box = createSlideBox(element, deleteAction, editAction);
		
		addToList(element, box, update);
	}
	
	protected void addToList(T element, SlideBox<T> box, boolean update) {
		box.addMouseListener(getMouseFocusListener());
		box.setRemoveConfirmer(getSlideBoxRemoveConfirmer());
		
		boxList.add(box);
		
		if (update)
			updateContainer();
		
		/* fire event */
		int index = elementList.indexOf(element);
		SlideBoxChangeEvent e = new SlideBoxChangeEvent(this, element, index);
		fireAddedListeners(e);
	}
	
	protected SlideBox<T> createSlideBox(T element,Action 
			deleteAction, Action editAction) {
		return new SlideBox<T>(element, deleteAction, editAction);
	}
	
	public void setElements(List<T> elements) {
		//reset first
		clearList();
		
		for (T element: elements) {
			addToList(element, false);
		}
		updateContainer();
	}
	
	public List<T> getElements() {
		return elementList;
	}

	
	protected void updateContainer() {
		container.removeAll();
		for (int i=boxList.size()-1; i>=0; i--) {

			SlideBox<T> box = boxList.get(i);
			DropTargetAdapter adp = new SlideBoxDropTargetAdapter(box);
			DropTarget trg = new SlideBoxDropTarget(box, adp);
			box.setDropTarget(trg);
			container.add(box);

		}
		
		if (boxList.size() >=  1) {
			SlideBox<T> box = boxList.get(boxList.size()-1);
			if (!box.isOpened()) {
				box.setPreferredSize(new Dimension(10000,30));
				if (getParent() != null)
					getParent().validate();
				box.revalidate();
				box.setPreferredSize(new Dimension(250,30));
			}
		} 
		
		revalidate();
	}
	
	public void removeFromList(int index) {
		if (index >= 0 && index < boxList.size()) {
			
			boxList.remove(index);
			Object element = elementList.remove(index);
		
			updateContainer();
			
			/* fire event */
			SlideBoxChangeEvent e = new SlideBoxChangeEvent(this, 
					element, index);
			fireRemovedListeners(e);
		}
	}
	
	public void removeFromList(T element) {
		int index = elementList.indexOf(element);		
		removeFromList(index);
	}
	
	public void removeFromList(SlideBox<T> box) {
		int index = boxList.indexOf(box);
		removeFromList(index);
	}
	
	public void moveListItem(int indexFrom, int indexTo) {
		/* remove */
		SlideBox<T> sb;
		T e;
		sb = boxList.remove(indexFrom);
		e = elementList.remove(indexFrom);
		
		/* insert */
		boxList.add(indexTo, sb);
		elementList.add(indexTo, e);
		
		updateContainer();
		
		/* fire event */
		SlideBoxChangeEvent ev = new SlideBoxChangeEvent(this, 
				indexFrom,
				indexTo);
		fireMovedListeners(ev);
	}
	
	protected int indexAtLocation(int x, int y) {
		Component comp = container.getComponentAt(x, y);
		
		if(comp instanceof SlideBox<?>) {
			SlideBox<?> box = (SlideBox<?>) comp;
			return boxList.indexOf(box);
		}
		
		return -1;
	}
	
	public void clearList() {
		elementList.clear();
		boxList.clear();
		updateContainer();
		
		/* fire event */
		SlideBoxChangeEvent e = new SlideBoxChangeEvent(this);
		fireClearedListeners(e);
	}
	
	public T getListElement(int index) {
		return elementList.get(index);
	}
	
	public SlideBox<T> getListBox(int index) {
		return boxList.get(index);
	}
	
	public int getListSize() {
		return boxList.size();
	}
	
	public Rectangle getBoundsAt(int index) {
		if (index < boxList.size())
			return boxList.get(index).getBounds();
		
		return null;
	}
	
	protected Component getComponentAt(int index) {
		if (index < boxList.size())
			return boxList.get(index);
		
		return null;
	}
	
	/**
	 * Sets enable state of box
	 * @param box box to enable/disable
	 * @param enabled enabled state
	 */
	protected void setEnabledBox(SlideBox<T> box, boolean enabled) {
		box.setEditEnabled(enabled);
		box.setDeleteEnabled(enabled);
		box.setDragEnabled(enabled);
	}
	
	public void startEdit(T element) {
		int indexOfEdited = elementList.indexOf(element);
		for (int i=0; i < boxList.size(); i++) {
			SlideBox<T> box = boxList.get(i);
			setEnabledBox(box, false);
			if (i==indexOfEdited) {
				box.setSelected(true);
			}
		}
		editIndex = indexOfEdited;
		//container.setFaded(true);
	}
	
	public void endEdit(T element) {		
		elementList.remove(editIndex);
		elementList.add(editIndex, element);
		for (int i=0; i < boxList.size(); i++) {
			SlideBox<T> box = boxList.get(i);
			setEnabledBox(box, true);
			if (i == editIndex) {
				box.setElement(element);
				box.updateBox();
				box.setSelected(false);
			}
		}
		
		/* fire event */
		SlideBoxChangeEvent e = new SlideBoxChangeEvent(this,
				element, editIndex);
		fireChangedListeners(e);
		
		editIndex = -1;
		//container.setFaded(false);
	}
	
	public void abortEdit() {
		for (int i=0; i < boxList.size(); i++) {
			SlideBox<T> box = boxList.get(i);
			setEnabledBox(box, true);
			if (i==editIndex) {
				box.setSelected(false);
			}
		}
		editIndex = -1;
		//container.setFaded(false);
	}
	
	private class DeleteAction extends AbstractAction {
		private static final long serialVersionUID = 2865906392257171855L;
		@SuppressWarnings("unchecked")
		@Override
		public void actionPerformed(ActionEvent e) {
			SlideBox<T> box = (SlideBox<T>) e.getSource();
			removeFromList(box);
		}
	}
	
	private class SlideBoxDropTargetAdapter extends DropTargetAdapter {
		private Component comp;
		SlideBoxDropTargetAdapter(Component c) {
			super();
			comp = c;
		}
		
		@SuppressWarnings("unchecked")
		@Override
		public void drop(DropTargetDropEvent dtde) {
			try {
				Object data = dtde.getTransferable()
						.getTransferData(SlideBox.getDataFlavor());
				
				SlideBox<T> draggedSlideBox = (SlideBox<T>)data;
				
				/* get target container index */
				int indexT = boxList.indexOf(comp);
				
				/* get source container index */
				int indexS = boxList.indexOf(draggedSlideBox);
				
				if(indexT < 0)
					indexT = boxList.size();
				
				moveListItem(indexS, indexT);
			} catch(Throwable e) {
				e.printStackTrace();
				dtde.rejectDrop();
				dtde.dropComplete(false);
			}
		}		
	};
	
	private class SlideBoxDropTarget extends DropTarget {
		private static final long serialVersionUID = -499885536636918349L;
		
		SlideBoxDropTarget(Component comp, DropTargetAdapter adapter) {
			super(comp, TransferHandler.COPY, adapter);
		}
		
		@Override
		public void dragOver(DropTargetDragEvent dtde) {
			try {
				Object data = dtde.getTransferable()
						.getTransferData(SlideBox.getDataFlavor());
				if(!(data instanceof SlideBox<?>))
					dtde.acceptDrag(TransferHandler.NONE);
				else
					dtde.acceptDrag(TransferHandler.COPY);
			} catch(Throwable e) {
				dtde.acceptDrag(TransferHandler.NONE);
			}
		}	
	};
	
	
	/**
	 * Adds the listener.
	 * @param vcl listener to add
	 */
	public void addSlideBoxChangeListener(SlideBoxChangeListener sbcl) {
		changeListenerList.add(SlideBoxChangeListener.class, sbcl);
	}
	
	/**
	 * Return an array of all the listeners of the given type.
	 * @param vcl listener to remove
	 */
	public void removeSlideBoxChangeListener(SlideBoxChangeListener sbcl) {
		changeListenerList.remove(SlideBoxChangeListener.class, sbcl);
	}
	
	/**
	 * Return an array of all the listeners. 
	 * @return
	 */
	protected SlideBoxChangeListener[] getSlideBoxChangeListener() {
		return changeListenerList.getListeners(SlideBoxChangeListener.class);
	}
	
	
	protected void fireRemovedListeners(SlideBoxChangeEvent event) {
		SlideBoxChangeListener[] listeners = getSlideBoxChangeListener();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].removedPerformed(event);
		}
	}
	
	protected void fireAddedListeners(SlideBoxChangeEvent event) {
		SlideBoxChangeListener[] listeners = getSlideBoxChangeListener();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].addedPerformed(event);
		}
	}
	
	protected void fireChangedListeners(SlideBoxChangeEvent event) {
		SlideBoxChangeListener[] listeners = getSlideBoxChangeListener();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].changedPerformed(event);
		}
	}
	
	protected void fireMovedListeners(SlideBoxChangeEvent event) {
		SlideBoxChangeListener[] listeners = getSlideBoxChangeListener();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].movedPerformed(event);
		}
	}
	
	protected void fireClearedListeners(SlideBoxChangeEvent event) {
		SlideBoxChangeListener[] listeners = getSlideBoxChangeListener();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].clearedPerformed(event);
		}
	}
	
	public interface RemoveConfirmer extends EventListener {
		public boolean canRemove(SlideBoxPanel<?> panel, 
				Object element, int index);
	}
}